using System;
using PluginScheduler.Interfaces;
using PluginScheduler.Jobs;
using PluginScheduler.Utils;

namespace PluginScheduler.Triggers
{
	
	[Serializable]
	public class SimpleTriggerImpl : ITrigger
	{
        public const int RepeatIndefinitely = -1;
        private const int YearToGiveupSchedulingAt = 2299;

        private DateTimeOffset? _nextFireTime;
		private DateTimeOffset? _previousFireTime;

        private int _repeatCount;
        private TimeSpan _repeatInterval = TimeSpan.Zero;
        private int _timesTriggered;
        private bool _complete;
	    private string _name;

        
        public SimpleTriggerImpl()
        {
        }
        
        
        public SimpleTriggerImpl(string name)
            : this(name, DateTime.Now, null, 0, TimeSpan.Zero)
        {
        }

        public SimpleTriggerImpl(string name, DateTimeOffset startTimeUtc)
            : this(name, startTimeUtc, null, 0, TimeSpan.Zero)
        {
        }
        
        public SimpleTriggerImpl(
            string name, 
            DateTimeOffset startTime,
            DateTimeOffset? endTime, 
            int repeatCount, 
            TimeSpan repeatInterval)
        {
            _name = name;
            _complete = false;
            StartTime = startTime;
            EndTime = endTime;
            RepeatCount = repeatCount;
            RepeatInterval = repeatInterval;
        }

        public int RepeatCount
        {
            get { return _repeatCount; }

            set
            {
                if (value < 0 && value != RepeatIndefinitely)
                {
                    throw new ArgumentException("Repeat count must be >= 0, use the constant RepeatIndefinitely for infinite.");
                }

                _repeatCount = value;
            }
        }

        public TimeSpan RepeatInterval
        {
            get { return _repeatInterval; }

            set
            {
                if (value < TimeSpan.Zero)
                {
                    throw new ArgumentException("Repeat interval must be >= 0");
                }

                _repeatInterval = value;
            }
        }

        public DateTimeOffset StartTime { get; set; }

	    public string Key { get; set; }

	    /// <summary>
	    /// Get or set the description given to the <see cref="ITrigger" /> instance by
	    /// its creator (if any).
	    /// </summary>
	    public string Description { get; set; }

	    /// <summary>
	    /// Get or set the <see cref="JobDataMap" /> that is associated with the 
	    /// <see cref="ITrigger" />.
	    /// <para>
	    /// Changes made to this map during job execution are not re-persisted, and
	    /// in fact typically result in an illegal state.
	    /// </para>
	    /// </summary>
	    public JobDataMap JobDataMap { get; set; }

	    /// <summary>
	    /// Returns the last time at which the <see cref="ITrigger" /> will fire, if
	    /// the Trigger will repeat indefinitely, null will be returned.
	    /// <para>
	    /// Note that the return time *may* be in the past.
	    /// </para>
	    /// </summary>
	    public DateTimeOffset? FinalFireTime { get; set; }

	    public DateTimeOffset? EndTime { get; set; }

        public virtual int TimesTriggered
        {
            get { return _timesTriggered; }
            set { _timesTriggered = value; }
        }

	    public IScheduleBuilder GetScheduleBuilder()
	    {
            var sb = SimpleScheduleBuilder.Create()
            .WithInterval(RepeatInterval)
            .WithRepeatCount(RepeatCount);
            return sb;
	    }

        public DateTimeOffset? FinalFireTimeUtc
        {
            get
            {
                if (_repeatCount == 0)
                {
                    return StartTime;
                }

                if (_repeatCount == RepeatIndefinitely && !EndTime.HasValue)
                {
                    return null;
                }

                if (_repeatCount == RepeatIndefinitely && !EndTime.HasValue)
                {
                    return null;
                }
                if (_repeatCount == RepeatIndefinitely)
                {
                    return GetFireTimeBefore(EndTime);
                }

                var lastTrigger = StartTime.AddMilliseconds(_repeatCount * _repeatInterval.TotalMilliseconds);

                if (!EndTime.HasValue || lastTrigger < EndTime.Value)
                {
                    return lastTrigger;
                }
                return GetFireTimeBefore(EndTime);
            }
        }

        public DateTimeOffset? GetFireTimeAfter(DateTimeOffset? afterTimeUtc)
        {
            if (_complete)
            {
                return null;
            }

            if ((_timesTriggered > _repeatCount) && (_repeatCount != RepeatIndefinitely))
            {
                return null;
            }

            if (!afterTimeUtc.HasValue)
            {
                afterTimeUtc = DateTime.Now;
            }

            if (_repeatCount == 0 && afterTimeUtc.Value.CompareTo(StartTime) >= 0)
            {
                return null;
            }

            DateTimeOffset startMillis = StartTime;
            DateTimeOffset afterMillis = afterTimeUtc.Value;
            DateTimeOffset endMillis = !EndTime.HasValue ? DateTimeOffset.MaxValue : EndTime.Value;


            if (endMillis <= afterMillis)
            {
                return null;
            }

            if (afterMillis < startMillis)
            {
                return startMillis;
            }

            var numberOfTimesExecuted = (long)(((long)(afterMillis - startMillis).TotalMilliseconds / _repeatInterval.TotalMilliseconds) + 1);

            if ((numberOfTimesExecuted > _repeatCount) &&
                (_repeatCount != RepeatIndefinitely))
            {
                return null;
            }

            DateTimeOffset time = startMillis.AddMilliseconds(numberOfTimesExecuted * _repeatInterval.TotalMilliseconds);

            if (endMillis <= time)
            {
                return null;
            }


            return time;
        }

        public DateTimeOffset? ComputeFirstFireTime()
    	{
			_nextFireTime = StartTime;

			while (true)
			{
                _nextFireTime = GetFireTimeAfter(_nextFireTime);

                if (!_nextFireTime.HasValue)
                {
                    break;
                }

                //avoid infinite loop
                if (_nextFireTime.Value.Year > YearToGiveupSchedulingAt)
                {
                    return null;
                }
			}

            return _nextFireTime;
		}
        
        public DateTimeOffset? GetNextFireTime()
		{
			return _nextFireTime;
		}

        public void SetNextFireTime(DateTimeOffset? nextFireTime)
	    {
            _nextFireTime = nextFireTime;
	    }

        public void SetPreviousFireTime(DateTimeOffset? previousFireTime)
	    {
            _previousFireTime = previousFireTime;
	    }
	    
	    public DateTimeOffset? GetPreviousFireTime()
	    {
	        return _previousFireTime;
	    }

	    public JobKey JobKey
	    {
	        get { throw new NotImplementedException(); }
	        set { throw new NotImplementedException(); }
	    }

	    public DateTimeOffset? GetFireTimeBefore(DateTimeOffset? end)
        {
            if (end == null) return null;

            if (end.Value < StartTime)
            {
                return null;
            }

            int numFires = ComputeNumTimesFiredBetween(StartTime, end);
            return StartTime.AddMilliseconds(numFires * _repeatInterval.TotalMilliseconds);
        }

		/// <summary>
        /// Computes the number of times fired between the two UTC date times.
        /// </summary>
        /// <param name="startTime">The start date and time.</param>
        /// <param name="endTime">The end date and time.</param>
        /// <returns></returns>
        public int ComputeNumTimesFiredBetween(DateTimeOffset? startTime, DateTimeOffset? endTime)
		{
			var time = (long) (endTime.Value - startTime.Value).TotalMilliseconds;
			return (int) (time/_repeatInterval.TotalMilliseconds);
		}

		public bool GetMayFireAgain()
		{
			return GetNextFireTime().HasValue;
		}
        
		public void Validate()
		{
			if (_repeatCount != 0 && _repeatInterval.TotalMilliseconds < 1)
			{
				throw new Exception("Repeat Interval cannot be zero.");
			}
		}

	    #region Implementation of ICloneable

	    public object Clone()
	    {
	        throw new NotImplementedException();
	    }

	    #endregion

	    #region Implementation of IComparable<ITrigger>

	    public int CompareTo(ITrigger other)
	    {
	        throw new NotImplementedException();
	    }

	    #endregion
	}
}
